<?php

/**
 * @copyright Michiel Hakvoort 2010
 * @license http://www.opensource.org/licenses/bsd-license.php New BSD
 * @package mangrove
 * @subpackage core
 * @filesource
 */

/*
 * Copyright (c) 2010 Michiel Hakvoort
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. The name of the author may not be used to endorse or promote products
 *    derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 */

namespace mg;

class ParseContext {

    /**
     * @var string
     */
    private $comment = null;

    /**
     * @var string
     */
    private $namespace = '';

    /**
     * @var array(string)
     */
    private $imports = array();

    private $file = null;

    /**
     * @var integer
     */
    private $modifiers = 0x00;

    public function getComment() {
        return $this->comment;
    }

    public function getNamespace() {
        return $this->namespace;
    }

    public function getImports() {
        return $this->imports;
    }

    public function getModifiers() {
        return $this->modifiers;
    }

    public function getFile() {
        return $this->file;
    }

    public function setComment($comment) {
        $this->comment = $comment;
    }

    public function setNamespace($namespace) {
        $this->namespace = $namespace;
    }

    public function setImports(array $imports) {
        $this->imports = $imports;
    }

    public function setModifiers($modifiers) {
        $this->modifiers = $modifiers;
    }

    public function setFile($file) {
        $this->file = $file;
    }
}

abstract class PhpParser implements PhpModifiers {

    protected static $ignored = array(Token :: T_WHITESPACE, Token :: T_COMMENT);

    /**
     *
     * @var mgTokenizer
     */
    protected $tokenizer = null;

    public function __construct(Tokenizer $tokenizer) {
        $this->tokenizer = $tokenizer;
    }

    /**
     * @param $ident
     *
     * @return mgToken The accepted token
     */
    protected function accept($ident) {
        if(!$this->tokenizer->valid()) {
            throw new ParseException('Unexpected end of tokens');
        }

        /* @var $current mgToken */
        $current = $this->tokenizer->current();

        if($ident !== null && $current->getIdent() !== $ident) {
            throw new ParseException('Expected \'' . Token :: getName($ident) . '\' got \''.Token :: getName($current->getIdent()).'\'');
        }

        $this->tokenizer->next();
        return $current;
    }

    protected function skipWhile() {
        $arguments = func_get_args();
        $tokens = array();

        foreach($arguments as $argument) {
            if(is_array($argument)) {
                $tokens = array_merge($tokens, $argument);
            } else {
                $tokens[] = $argument;
            }
        }

        $stop = false;

        while($this->tokenizer->valid() && !$stop) {
            /* @var $current mgToken */
            $current = $this->tokenizer->current();

            $stop = !in_array($current->getIdent(), $tokens, true);

            if(!$stop) {
                $this->tokenizer->next();
            }
        }

        return $this->tokenizer->current();
    }

    protected function skipUntil() {
        $arguments = func_get_args();
        $tokens = array();

        foreach($arguments as $argument) {
            if(is_array($argument)) {
                $tokens = array_merge($tokens, $argument);
            } else {
                $tokens[] = $argument;
            }
        }

        $found = false;

        while($this->tokenizer->valid() && !$found) {
            /* @var $current mgToken */
            $current = $this->tokenizer->current();
            $found = in_array($current->getIdent(), $tokens, true);

            if(!$found) {
                $this->tokenizer->next();
            }
        }

        return $this->tokenizer->current();
    }
}

class ParseException extends Exception {

}

class PhpFileParser extends PhpParser {

    private $file = null;
    public function __construct(Tokenizer $tokenizer, $file) {
        parent :: __construct($tokenizer);
        $this->file = $file;
    }

    public function parse() {
        $this->tokenizer->rewind();

        /* @var $token mgToken */
        $token = $this->skipWhile(self :: $ignored);

        $parseContext = new ParseContext();
        $parseContext->setFile($this->file);

        $result = array();

        while($this->tokenizer->valid()) {
            switch($token->getIdent()) {
                case Token :: T_CLASS :
                    $this->accept(Token :: T_CLASS);
                    $classParser = new PhpClassParser($this->tokenizer, $parseContext);

                    $result[] = $classParser->parse();

                    break;

                case Token :: T_INTERFACE :
                    $this->accept(Token :: T_INTERFACE);

                    $interfaceParser = new PhpInterfaceParser($this->tokenizer, $parseContext);

                    $result[] = $interfaceParser->parse();

                    break;

                case Token :: T_NAMESPACE :
                    $this->accept(Token :: T_NAMESPACE);
                    $namespaceParser = new PhpNamespaceParser($this->tokenizer);

                    $namespace = $namespaceParser->parse();

                    if($namespace !== null) {
                        $parseContext->setNamespace($namespace);
                        $parseContext->setImports(array());
                    }

                    break;

                case Token :: T_USE :
                    $this->accept(Token :: T_USE);

                    $useParser = new PhpUseParser($this->tokenizer);
                    $imports = $useParser->parse();

                    $parseContext->setImports($parseContext->getImports() + $imports);

                    break;

                    // Ignore functions
                case Token :: T_FUNCTION :
                    $this->accept(Token :: T_FUNCTION);

                    $depth = 0;
                    do {
                        $token = $this->skipUntil(Token :: T_CURLY_OPEN, Token :: T_CURLY_CLOSE);
                        if($token === Token :: T_CURLY_OPEN) {
                            $depth++;
                        } else {
                            $depth--;
                        }

                    } while($depth === 0);
                    	
                    break;
                case Token :: T_ABSTRACT :
                    $this->accept(Token :: T_ABSTRACT);
                    $parseContext->setModifiers($parseContext->getModifiers() | self :: IS_ABSTRACT);

                    break;

                case Token :: T_FINAL :
                    $this->accept(Token :: T_FINAL);
                    $parseContext->setModifiers($parseContext->getModifiers() | self :: IS_FINAL);

                    break;

                case Token :: T_DOC_COMMENT :
                    $this->accept(Token :: T_DOC_COMMENT);
                    $parseContext->setComment($token->getValue());

                    break;

                default :
                    $this->accept($token->getIdent());
                    $parseContext->setComment(null);
            }

            $token = $this->skipWhile(self :: $ignored);
        }

        return $result;
    }
}

/**
 * Parse a PHP 'namespace' definition;
 *
 * @author Michiel Hakvoort
 *
 */
class PhpNamespaceParser extends PhpParser {

    private $isNamespaceDeclaration = false;

    public function __construct(Tokenizer $tokenizer) {
        parent :: __construct($tokenizer);
    }

    /**
     *
     * @return string|null
     */
    public function parse() {
        $accepted = array(Token :: T_NS_SEPARATOR, Token :: T_STRING, Token :: T_SEMICOLON, Token :: T_CURLY_OPEN);

        /* @var $token mgToken */
        $token = $this->skipWhile(self :: $ignored, Token :: T_DOC_COMMENT);

        if(!$this->tokenizer->valid()) {
            return null;
        }

        switch($token->getIdent()) {
            case Token :: T_CURLY_OPEN :
            case Token :: T_STRING :
            case Token :: T_NS_SEPARATOR :
                $this->isNamespaceDeclaration = true;
        }

        if(!$this->isNamespaceDeclaration) {
            return null;
        }

        $result = '';

        $hasNamespace = false;

        // namespace is followed by T_CURLY_OPEN or T_STRING


        while(!$hasNamespace && $this->tokenizer->valid()) {
            	
            switch($token->getIdent()) {
                case Token :: T_NS_SEPARATOR :
                    $this->accept(Token :: T_NS_SEPARATOR);
                    $result .= '\\';

                    break;

                case Token :: T_STRING :
                    $this->accept(Token :: T_STRING);
                    $result .= $token->getValue();

                    break;

                case Token :: T_SEMICOLON :
                    $this->accept(Token :: T_SEMICOLON);
                    $hasNamespace = true;

                    break;

                case Token :: T_CURLY_OPEN :
                    $this->accept(Token :: T_CURLY_OPEN);
                    $hasNamespace = true;

                    break;

                default :

            }
            	
            if(!$hasNamespace) {
                $token = $this->skipUntil($accepted);
            }
        }

        if(!$hasNamespace) {
            throw new ParseException('Unable to parse namespace, not finished with semicolon');
        }

        return $result;
    }
}

/**
 * Parse a PHP use definition
 *
 * example : use \name\space as ident, other\name\space
 *
 * @author Michiel Hakvoort
 *
 */
class PhpUseParser extends PhpParser {

    public function __construct(Tokenizer $tokenizer) {
        parent :: __construct($tokenizer);
    }

    /**
     *
     * @return array(string)
     */
    public function parse() {
        $result = array();

        $finished = false;

        $this->skipWhile(self :: $ignored, Token :: T_DOC_COMMENT);

        $namespace = '';
        $alias = '';

        $namespaceDefined = false;

        while(!$finished && $this->tokenizer->valid()) {

            /* @var $token \mg\Token */
            $token = $this->tokenizer->current();

            switch($token->getIdent()) {
                case Token :: T_SEMICOLON :
                    $this->accept(Token :: T_SEMICOLON);
                    $finished = true;
                    $result[$alias] = $namespace;

                    break;

                case Token :: T_COMMA :
                    $this->accept(Token :: T_COMMA);
                    $namespaceDefined = false;
                    $result[$alias] = $namespace;

                    $namespace = '';
                    $alias = '';

                    break;

                case Token :: T_STRING :
                    $this->accept(Token :: T_STRING);
                    	
                    if(!$namespaceDefined) {
                        $namespace .= $token->getValue();
                    }

                    $alias = $token->getValue();

                    break;

                case Token :: T_NS_SEPARATOR :
                    $this->accept(Token :: T_NS_SEPARATOR);
                    	
                    if(!$namespaceDefined) {
                        $namespace .= '\\';
                    }

                    break;

                case Token :: T_AS :
                    $this->accept(Token :: T_AS);
                    $namespaceDefined = true;
                    	
                    break;

                default :
                    throw new ParseException('Unexpected token : ' . $token);
            }
            	
            $this->skipWhile(self :: $ignored, Token :: T_DOC_COMMENT);
        }

        return $result;
    }

}

class PhpInterfaceParser extends PhpParser {

    /**
     * @var mgParseContext
     */
    private $parseContext = null;

    public function __construct(Tokenizer $tokenizer, ParseContext $parseContext) {
        parent :: __construct($tokenizer);

        $this->parseContext = $parseContext;
    }

    public function parse() {
        $methods = array();
        $constants = array();

        $extends = array();

        $context = new ParseContext();
        $context->setImports($this->parseContext->getImports());
        $context->setNamespace($this->parseContext->getNamespace());

        $this->skipWhile(self :: $ignored, Token :: T_DOC_COMMENT);

        /* @var $token mgToken */
        $token = $this->tokenizer->current();

        $this->accept(Token :: T_STRING);

        $className = $token->getValue();

        $this->skipWhile(self :: $ignored, Token :: T_DOC_COMMENT);

        $token = $this->tokenizer->current();

        // parse ... implements \some\identifier, other\identifier
        if($token->getIdent() === Token :: T_EXTENDS) {
            $this->accept(Token :: T_EXTENDS);

            $hasCurlyOpen = false;

            while(!$hasCurlyOpen) {
                $this->skipWhile(self :: $ignored, Token :: T_DOC_COMMENT);
                $token = $this->tokenizer->current();

                switch($token->getIdent()) {
                    case Token :: T_CURLY_OPEN :
                        $hasCurlyOpen = true;
                        continue 2;
                    case Token :: T_COMMA :
                        $this->accept(Token :: T_COMMA);
                        continue 2;
                    default :
                }

                $identifierParser = new PhpIdentifierParser($this->tokenizer, $this->parseContext);
                $extends[]= $identifierParser->parse();
            }
        }

        $this->accept(Token :: T_CURLY_OPEN);

        $finished = false;

        $this->skipWhile(self :: $ignored);
        $token = $this->tokenizer->current();

        while(!$finished && $this->tokenizer->valid()) {

            switch($token->getIdent()) {
                case Token :: T_CURLY_CLOSE :
                    $finished = true;

                    continue 2;

                    /*
                     * parse ... const myconst = 'val';
                     */
                case Token :: T_CONST :
                    $this->accept(Token :: T_CONST);

                    $expectString = true;

                    while($expectString) {
                        $token = $this->skipWhile(self :: $ignored, Token :: T_DOC_COMMENT);
                        $this->accept(Token :: T_STRING);

                        $const = $token->getValue();

                        $assignmentParser = new PhpAssignmentParser($this->tokenizer);
                        $value = $assignmentParser->parse();

                        $token = $this->skipWhile(self :: $ignored, Token :: T_DOC_COMMENT);

                        if($value === null) {
                            throw new ParseException("Assignment expected");
                        }

                        $constants[$const] = $value;

                        if($token->getIdent() === Token :: T_COMMA) {
                            $this->accept(Token :: T_COMMA);
                            continue;
                        }

                        $expectString = false;
                    }

                    $this->accept(Token :: T_SEMICOLON);

                    $context->setModifiers(0x00);
                    $context->setComment(null);

                    break;

                    /*
                     * parse ... var "$..."
                     */
                case Token :: T_VAR :
                    $this->accept(Token :: T_VAR);

                    $context->setModifiers(self :: T_PUBLIC);

                    break;

                    /*
                     * Class variable
                     */
                case Token :: T_VARIABLE :
                    $expectVariable = true;

                    while($expectVariable) {
                        $this->accept(Token :: T_VARIABLE);

                        $variable = $token->getValue();

                        $assignmentParser = new PhpAssignmentParser($this->tokenizer);

                        $value = $assignmentParser->parse();

                        $token = $this->skipWhile(self :: $ignored, Token :: T_DOC_COMMENT);

                        $properties[] = new RawStaticSourceProperty($variable, $value, $context->getModifiers(), $context->getComment());

                        if($token->getIdent() === Token :: T_COMMA) {
                            $this->accept(Token :: T_COMMA);
                            $token = $this->skipWhile(self :: $ignored, Token :: T_DOC_COMMENT);
                            continue;
                        }

                        $expectVariable = false;
                    }
                    	
                    $this->accept(Token :: T_SEMICOLON);
                    $context->setModifiers(0x00);

                    $context->setComment(null);
                    break;

                    /*
                     * Class method
                     */
                case Token :: T_FUNCTION :
                    $this->accept(Token :: T_FUNCTION);

                    $methodParser = new PhpMethodParser($this->tokenizer, $context);

                    $method = $methodParser->parse();

                    $methods[] = $method;
                    	
                    $context->setModifiers(0x00);
                    $context->setComment(null);

                    break;

                    /*
                     * Modifiers
                     */
                case Token :: T_PUBLIC :
                    $this->accept(Token :: T_PUBLIC);
                    	
                    $context->setModifiers($context->getModifiers() | self :: IS_PUBLIC);
                    	
                    break;

                case Token :: T_STATIC :
                    $this->accept(Token :: T_STATIC);

                    $context->setModifiers($context->getModifiers() | self :: IS_STATIC);

                    break;
                    	
                case Token :: T_PROTECTED :
                    $this->accept(Token :: T_PROTECTED);

                    $context->setModifiers($context->getModifiers() | self :: IS_PROTECTED);
                    	
                    break;
                    	
                case Token :: T_PRIVATE :
                    $this->accept(Token :: T_PRIVATE);

                    $context->setModifiers($context->getModifiers() | self :: IS_PRIVATE);
                    	
                    break;

                case Token :: T_FINAL :
                    $this->accept(Token :: T_FINAL);

                    $context->setModifiers($context->getModifiers() | self :: IS_FINAL);

                    break;

                case Token :: T_ABSTRACT :
                    $this->accept(Token :: T_ABSTRACT);

                    $context->setModifiers($context->getModifiers() | self :: IS_ABSTRACT);

                    break;

                case Token :: T_DOC_COMMENT :
                    $this->accept(Token :: T_DOC_COMMENT);
                    $context->setComment($token->getValue());
                    	
                    break;

                default :
                    throw new ParseException("?");
            }
            $this->skipWhile(self :: $ignored);
            $token = $this->tokenizer->current();
        }

        $this->accept(Token :: T_CURLY_CLOSE);

        $result = new RawStaticSourceClass($this->parseContext->getFile(), $this->parseContext->getNamespace(), $className, null, $extends, array(), $constants, $methods, PhpModifiers :: IS_INTERFACE, $this->parseContext->getComment());

        return $result;
    }
}

/**
 * Parse a php class definition
 *
 *
 * @author Michiel Hakvoort
 *
 */
class PhpClassParser extends PhpParser {

    /**
     * @var mgParseContext
     */
    private $parseContext = null;

    public function __construct(Tokenizer $tokenizer, ParseContext $parseContext) {
        parent :: __construct($tokenizer);

        $this->parseContext = $parseContext;
    }

    /**
     *
     * @return mgRawStaticSourceClass
     */
    public function parse() {
        $methods = array();
        $constants = array();
        $properties = array();

        $extends = null;
        $implements = array();

        $context = new ParseContext();
        $context->setImports($this->parseContext->getImports());
        $context->setNamespace($this->parseContext->getNamespace());

        $this->skipWhile(self :: $ignored, Token :: T_DOC_COMMENT);

        /* @var $token mgToken */
        $token = $this->tokenizer->current();

        $this->accept(Token :: T_STRING);

        $className = $token->getValue();

        $this->skipWhile(self :: $ignored, Token :: T_DOC_COMMENT);

        $token = $this->tokenizer->current();

        // parse ... extends \some\identifier ...
        if($token->getIdent() === Token :: T_EXTENDS) {
            $this->accept(Token :: T_EXTENDS);
            	
            $identifierParser = new PhpIdentifierParser($this->tokenizer, $this->parseContext);

            $extends = $identifierParser->parse();
            	
            $this->skipWhile(self :: $ignored, Token :: T_DOC_COMMENT);
            $token = $this->tokenizer->current();
        }

        // parse ... implements \some\identifier, other\identifier
        if($token->getIdent() === Token :: T_IMPLEMENTS) {
            $this->accept(Token :: T_IMPLEMENTS);

            $hasCurlyOpen = false;

            while(!$hasCurlyOpen) {
                $this->skipWhile(self :: $ignored, Token :: T_DOC_COMMENT);
                $token = $this->tokenizer->current();

                switch($token->getIdent()) {
                    case Token :: T_CURLY_OPEN :
                        $hasCurlyOpen = true;
                        continue 2;
                    case Token :: T_COMMA :
                        $this->accept(Token :: T_COMMA);
                        continue 2;
                    default :
                }

                $identifierParser = new PhpIdentifierParser($this->tokenizer, $this->parseContext);
                $implements[]= $identifierParser->parse();
            }
        }

        $this->accept(Token :: T_CURLY_OPEN);

        $finished = false;

        $this->skipWhile(self :: $ignored);
        $token = $this->tokenizer->current();

        while(!$finished && $this->tokenizer->valid()) {
            switch($token->getIdent()) {
                case Token :: T_CURLY_CLOSE :
                    $finished = true;

                    continue 2;

                    /*
                     * parse ... const myconst = 'val';
                     */
                case Token :: T_CONST :
                    $this->accept(Token :: T_CONST);

                    $expectString = true;

                    while($expectString) {
                        $token = $this->skipWhile(self :: $ignored, Token :: T_DOC_COMMENT);
                        $this->accept(Token :: T_STRING);

                        $const = $token->getValue();

                        $assignmentParser = new PhpAssignmentParser($this->tokenizer);
                        $value = $assignmentParser->parse();

                        $token = $this->skipWhile(self :: $ignored, Token :: T_DOC_COMMENT);

                        if($value === null) {
                            throw new ParseException("Assignment expected");
                        }

                        $constants[$const] = $value;

                        if($token->getIdent() === Token :: T_COMMA) {
                            $this->accept(Token :: T_COMMA);
                            continue;
                        }

                        $expectString = false;
                    }

                    $this->accept(Token :: T_SEMICOLON);

                    $context->setModifiers(0x00);
                    $context->setComment(null);

                    break;

                    /*
                     * parse ... var "$..."
                     */
                case Token :: T_VAR :
                    $this->accept(Token :: T_VAR);

                    $context->setModifiers(self :: IS_PUBLIC);

                    break;

                    /*
                     * Class variable
                     */
                case Token :: T_VARIABLE :
                    $expectVariable = true;

                    while($expectVariable) {
                        $this->accept(Token :: T_VARIABLE);

                        $variable = $token->getValue();

                        $assignmentParser = new PhpAssignmentParser($this->tokenizer);

                        $value = $assignmentParser->parse();

                        $token = $this->skipWhile(self :: $ignored, Token :: T_DOC_COMMENT);

                        $properties[] = new RawStaticSourceProperty($variable, $value, $context->getModifiers(), $context->getComment());

                        if($token->getIdent() === Token :: T_COMMA) {
                            $this->accept(Token :: T_COMMA);
                            $token = $this->skipWhile(self :: $ignored, Token :: T_DOC_COMMENT);
                            continue;
                        }

                        $expectVariable = false;
                    }
                    	
                    $this->accept(Token :: T_SEMICOLON);
                    $context->setModifiers(0x00);

                    $context->setComment(null);
                    break;

                    /*
                     * Class method
                     */
                case Token :: T_FUNCTION :
                    $this->accept(Token :: T_FUNCTION);

                    $methodParser = new PhpMethodParser($this->tokenizer, $context);

                    $method = $methodParser->parse();

                    $methods[] = $method;
                    	
                    $context->setModifiers(0x00);
                    $context->setComment(null);

                    break;

                    /*
                     * Modifiers
                     */
                case Token :: T_PUBLIC :
                    $this->accept(Token :: T_PUBLIC);
                    	
                    $context->setModifiers($context->getModifiers() | self :: IS_PUBLIC);
                    	
                    break;

                case Token :: T_STATIC :
                    $this->accept(Token :: T_STATIC);

                    $context->setModifiers($context->getModifiers() | self :: IS_STATIC);

                    break;
                    	
                case Token :: T_PROTECTED :
                    $this->accept(Token :: T_PROTECTED);

                    $context->setModifiers($context->getModifiers() | self :: IS_PROTECTED);
                    	
                    break;
                    	
                case Token :: T_PRIVATE :
                    $this->accept(Token :: T_PRIVATE);

                    $context->setModifiers($context->getModifiers() | self :: IS_PRIVATE);
                    	
                    break;

                case Token :: T_FINAL :
                    $this->accept(Token :: T_FINAL);

                    $context->setModifiers($context->getModifiers() | self :: IS_FINAL);

                    break;

                case Token :: T_ABSTRACT :
                    $this->accept(Token :: T_ABSTRACT);

                    $context->setModifiers($context->getModifiers() | self :: IS_ABSTRACT);

                    break;

                case Token :: T_DOC_COMMENT :
                    $this->accept(Token :: T_DOC_COMMENT);
                    $context->setComment($token->getValue());
                    	
                    break;

                    	
                default :
                    throw new ParseException("Unexpected token : {$token}");
            }
            $this->skipWhile(self :: $ignored);
            $token = $this->tokenizer->current();
        }

        $this->accept(Token :: T_CURLY_CLOSE);
        $result = new RawStaticSourceClass($this->parseContext->getFile(), $this->parseContext->getNamespace(), $className, $extends, $implements, $properties, $constants, $methods, $this->parseContext->getModifiers(), $this->parseContext->getComment());

        return $result;

    }

}

class PhpIdentifierParser extends PhpParser {

    /**
     *
     * @var mgParseContext
     */
    private $parseContext = null;

    public function __construct(Tokenizer $tokenizer, ParseContext $parseContext) {
        parent :: __construct($tokenizer);

        $this->parseContext = $parseContext;

    }

    /**
     *
     * @return string
     */
    public function parse() {

        $imports = $this->parseContext->getImports();
        $namespace = $this->parseContext->getNamespace();

        $identifier = null;

        $identifierFinished = false;

        /* @var $token mgToken */
        $token = $this->skipWhile(self :: $ignored, Token :: T_DOC_COMMENT);

        while(!$identifierFinished && $this->tokenizer->valid()) {
            switch($token->getIdent()) {
                case Token :: T_NS_SEPARATOR :
                    $this->accept(Token :: T_NS_SEPARATOR);

                    if($identifier === null) {
                        $identifier = '';
                    } else {
                        $identifier .= '\\';
                    }

                    break;

                case Token :: T_NAMESPACE :
                    $this->accept(Token :: T_NAMESPACE);

                    if($identifier === null) {
                        $identifier = $namespace;
                    } else {
                        throw new ParseException('Unexpected token T_NAMESPACE');
                    }

                    break;

                case Token :: T_STRING :
                    $this->accept(Token :: T_STRING);
                    $value = $token->getValue();
                    if($identifier === null) {
                        if(isset($imports[$value])) {
                            $identifier = $imports[$value];
                        } else {
                            if($namespace !== '') {
                                $identifier = $namespace . '\\' . $value;
                            } else {
                                $identifier = $value;
                            }
                        }
                    } else {
                        $identifier .= $value;
                    }

                    break;

                default :
                    $identifierFinished = true;
            }

            $this->skipWhile(self :: $ignored, Token :: T_DOC_COMMENT);
            $token = $this->tokenizer->current();
        }

        return $identifier;
    }

}

class PhpMethodParser extends PhpParser {

    private $parseContext = null;

    public function __construct(Tokenizer $tokenizer, ParseContext $parseContext) {
        parent :: __construct($tokenizer);

        $this->parseContext = $parseContext;
    }

    public function parse() {

        /* @var $token mgToken */
        $token = $this->skipWhile(self :: $ignored, Token :: T_DOC_COMMENT);

        // returns reference
        $byReference = false;

        if($token->getIdent() === Token :: T_OPERATOR && $token->getValue() === '&') {
            $byReference = true;
            $this->accept(Token :: T_OPERATOR);
            $token = $this->skipWhile(self :: $ignored, Token :: T_DOC_COMMENT);
        }

        // name
        $name = $token->getValue();

        $this->accept(Token :: T_STRING);

        $this->skipWhile(self :: $ignored, Token :: T_DOC_COMMENT);

        $this->accept(Token :: T_PARENTHESIS_OPEN);

        $hasParenthesisClose = false;

        // parameters
        $parameters = array();

        // collect parameters
        while(!$hasParenthesisClose & $this->tokenizer->valid()) {
            $this->skipWhile(self :: $ignored, Token :: T_DOC_COMMENT);
            $token = $this->tokenizer->current();

            switch($token->getIdent()) {
                case Token :: T_PARENTHESIS_CLOSE :
                    $hasParenthesisClose = true;
                    continue 2;
                case Token :: T_COMMA :
                    $this->accept(Token :: T_COMMA);
                    continue 2;
                default :
            }

            // parameter is by reference
            $parameterByReference = false;
            // parameter is of array type
            $parameterIsArray = false;
            	
            // parameter is of `identifier` type
            $identifier = null;

            switch($token->getIdent()) {
                case Token :: T_ARRAY :
                    $this->accept(Token :: T_ARRAY);
                    $parameterIsArray = true;
                    $token = $this->skipWhile(self :: $ignored, Token :: T_DOC_COMMENT);

                    break;

                case Token :: T_STRING :
                case Token :: T_NS_SEPARATOR :
                case Token :: T_NAMESPACE :
                    $identifierParser = new PhpIdentifierParser($this->tokenizer, $this->parseContext);
                    	
                    $identifier = $identifierParser->parse();
                    	
                    $token = $this->skipWhile(self :: $ignored, Token :: T_DOC_COMMENT);

                    break;

                default :
            }

            if($token->getIdent() === Token :: T_OPERATOR && $token->getValue() === '&') {
                $this->accept(Token :: T_OPERATOR);
                $parameterByReference = true;
                $token = $this->skipWhile(self :: $ignored, Token :: T_DOC_COMMENT);
            }
            	
            // variable name
            $variableName = $token->getValue();
            	
            $this->accept(Token :: T_VARIABLE);
            	
            // possible assignment
            $assignmentParser = new PhpAssignmentParser($this->tokenizer);
            $value = $assignmentParser->parse();
            // no assigment
            $allowsNull = false;
            if($value === null) {

            } elseif(trim(mb_strtolower($value)) === 'null') {
                $allowsNull = true;
            }
            	
            $parameters[] = new RawStaticSourceParameter($variableName, $parameterIsArray, $identifier, $parameterByReference, $value, $allowsNull);

        }

        $this->accept(Token :: T_PARENTHESIS_CLOSE);

        $token = $this->skipWhile(self :: $ignored, Token :: T_DOC_COMMENT);

        // find the start and end of the method body (either ';' or '{' ... '}')
        switch($token->getIdent()) {
            case Token :: T_CURLY_OPEN :
                $gotCurlyClose = false;

                $this->accept(Token :: T_CURLY_OPEN);
                $token = $this->skipWhile(self :: $ignored);

                $depth = 0;

                while($this->tokenizer->valid() && !$gotCurlyClose) {
                    switch($token->getIdent()) {
                        case Token :: T_CURLY_CLOSE :
                            $this->accept(Token :: T_CURLY_CLOSE);
                            if($depth === 0) {
                                $gotCurlyClose = true;
                                continue 2;
                            }
                            $depth--;

                            break;

                        case Token :: T_CURLY_OPEN :
                            $this->accept(Token :: T_CURLY_OPEN);
                            $depth++;

                            break;

                        case Token :: T_DOLLAR_OPEN_CURLY_BRACES :
                            $this->accept(Token :: T_DOLLAR_OPEN_CURLY_BRACES);
                            $depth++;
                            	
                            break;

                        default :
                            $this->accept($token->getIdent());
                    }
                    	
                    $token = $this->skipWhile(self :: $ignored);
                }


                break;
            case Token :: T_SEMICOLON :
                $this->accept(Token :: T_SEMICOLON);
                break;
        }

        return new RawStaticSourceMethod($name, $byReference, $this->parseContext->getModifiers(), $parameters, $this->parseContext->getComment());

    }
}

class PhpAssignmentParser extends PhpParser {

    public function __construct(Tokenizer $tokenizer) {
        parent :: __construct($tokenizer);
    }

    /**
     *
     * @return string|null
     */
    public function parse() {

        /* @var $token mgToken */
        $token = $this->skipWhile(self :: $ignored, Token :: T_DOC_COMMENT);

        if(!$this->tokenizer->valid()) {
            return;
        }

        switch($token->getIdent()) {
            case Token :: T_SEMICOLON :
            case Token :: T_COMMA :
            case Token :: T_PARENTHESIS_CLOSE :
                return null;
            case Token :: T_ASSIGNMENT :
                $this->accept(Token :: T_ASSIGNMENT);
                break;
            default :
                throw new ParseException('Unexpected token ' . $token);
        }

        $finished = false;

        $token = $this->tokenizer->current();

        $value = '';

        $depth = 0;

        // ( || { => depth++, ) || } => depth--
        // , || ; && depth > 0, no exit


        // ) || ; || , at depth = 0 = halt
        while(!$finished && $this->tokenizer->valid()) {

            switch($token->getIdent()) {
                case Token :: T_COMMA :
                case Token :: T_SEMICOLON :
                    if($depth === 0) {
                        $finished = true;
                        continue 2;
                    }

                    break;

                case Token :: T_DOLLAR_OPEN_CURLY_BRACES :
                case Token :: T_CURLY_OPEN :
                case Token :: T_PARENTHESIS_OPEN :
                    $depth++;

                    break;

                case Token :: T_CURLY_CLOSE :
                    $depth--;

                    break;
                case Token :: T_PARENTHESIS_CLOSE :
                    if($depth === 0) {
                        $finished = true;
                        continue 2;
                    }
                    $depth--;
                    	
                    break;

                default:
            }

            $value .= $token->getValue();

            $this->accept($token->getIdent());

            $token = $this->tokenizer->current();

        }

        if(mb_strtolower(trim($value)) === 'null') {
            $this->isNull = true;
        }

        return $value;
    }

}
// Wrappers for the analyzed source code

class RawStaticSourceInterface {

    private $namespace = null;
    private $name = null;
    private $interfaces = null;
    private $constants = null;
    private $methods = null;
    private $comment = null;

    public function __construct($namespace, $name, $interfaces, $constants, $methods, $comment) {
        $this->namespace = $namespace;
        $this->name = $name;
        $this->interfaces = $interfaces;
        $this->constants = $constants;
        $this->methods = $methods;
        $this->comment = $comment;
    }

}

class RawStaticSourceClass {

    private $file = null;
    private $namespace = null;
    private $name = null;
    private $parent = null;
    private $interfaces = null;
    private $properties = null;
    private $constants = null;
    private $methods = null;
    private $modifiers = null;
    private $comment = null;

    public function __construct($file, $namespace, $name, $parent, $interfaces, $properties, $constants, $methods, $modifiers, $comment) {
        $this->file = $file;

        // TODO, fix this elsewhere
        if(mb_strlen($namespace) > 0 && $namespace{0} === '\\') {
            $namespace = mb_substr($namespace, 1);
        }

        if($parent !== null && mb_strlen($parent) > 0 && $parent{0} === '\\') {
            $parent = mb_substr($parent, 1);
        }

        foreach($interfaces as &$interface) {
            if(mb_strlen($interface) > 0 && $interface{0} === '\\') {
                $interface = mb_substr($interface, 1);
            }
        }

        $this->namespace = $namespace;
        $this->name = $name;
        $this->parent = $parent;
        $this->interfaces = $interfaces;
        $this->properties = $properties;
        $this->constants = $constants;
        $this->methods = $methods;
        $this->modifiers = $modifiers;
        $this->comment = $comment;
    }

    /**
     *
     * @return string
     */
    public function getName() {
        return $this->name;
    }

    /**
     *
     * @return string
     */
    public function getQualifiedName() {
        if($this->namespace !== '') {
            return $this->namespace . '\\'. $this->name;
        } else {
            return $this->name;
        }
    }

    /**
     *
     * @return string
     */
    public function getNamespace() {
        return $this->namespace;
    }

    /**
     *
     * @return array(mgRawStaticSourceMethod)
     */
    public function getMethods() {
        return $this->methods;
    }

    /**
     * @return array(string)
     */
    public function getImplementedInterfaces() {
        return $this->interfaces;
    }

    /**
     * @return string
     */
    public function getParentClass() {
        return $this->parent;
    }

    /**
     *
     * @return array(mgRawStaticSourceProperty)
     */
    public function getProperties() {
        return $this->properties;
    }

    /**
     *
     * @return array(string=>string)
     */
    public function getConstants() {
        return $this->constants;
    }

    public function getModifiers() {
        return $this->modifiers;
    }

    public function getFile() {
        return $this->file;
    }
}

class RawStaticSourceMethod implements PhpModifiers {

    private $name = null;
    private $isByReference = null;
    private $modifiers = null;
    private $parameters = null;
    private $comment = null;

    public function __construct($name, $isByReference, $modifiers, $parameters, $comment) {
        $this->name = $name;
        $this->isByReference = $isByReference;
        $this->modifiers = $modifiers;
        $this->parameters = $parameters;
        $this->comment = $comment;
    }

    /**
     *
     * @return string
     */
    public function getName() {
        return $this->name;
    }

    /**
     *
     * @return integer
     */
    public function getModifiers() {
        return $this->modifiers;
    }

    /**
     *
     * @return boolean
     */
    public function isByReference() {
        return $this->isByReference;
    }

    /**
     * @return boolean
     */
    public function isAbstract() {
        return (boolean) ($this->modifiers & self :: IS_ABSTRACT);
    }

    /**
     * @return boolean
     */
    public function isFinal() {
        return (boolean) ($this->modifiers & self :: IS_FINAL);
    }

    /**
     * @return boolean
     */
    public function isPublic() {
        return (boolean) ($this->modifiers & self :: IS_PUBLIC);
    }

    /**
     * @return boolean
     */
    public function isPrivate() {
        return (boolean) ($this->modifiers & self :: IS_PRIVATE);
    }

    /**
     * @return boolean
     */
    public function isProtected() {
        return (boolean) ($this->modifiers & self :: IS_PROTECTED);
    }

    /**
     * @return boolean
     */
    public function isStatic() {
        return (boolean) ($this->modifiers & self :: IS_STATIC);
    }

    /**
     * @return array(mgRawStaticSourceParameter)
     */
    public function getParameters() {
        return $this->parameters;
    }

}

class RawStaticSourceProperty {

    private $name = null;
    private $value = null;
    private $modifiers = null;
    private $comment = null;

    public function __construct($name, $value, $modifiers, $comment) {
        $this->name = $name;
        $this->value = $value;
        $this->modifiers = $modifiers;
        $this->comment = $comment;
    }
}

class RawStaticSourceParameter {

    private $name = null;
    private $isArray = null;
    private $identifierType = null;
    private $isPassedByReference = null;
    private $defaultValue = null;
    private $allowsNull = null;

    public function __construct($name, $isArray, $identifierType, $isPassedByReference, $defaultValue, $allowsNull) {
        $this->name = $name;
        $this->isArray = $isArray;
        $this->identifierType = $identifierType;
        $this->isPassedByReference = $isPassedByReference;
        $this->defaultValue = $defaultValue;
        $this->allowsNull = $allowsNull;
    }

    /**
     * @return string
     */
    public function getName() {
        return $this->name;
    }

    /**
     * @return boolean
     */
    public function isArray() {
        return $this->isArray;
    }

    /**
     * @return boolean
     */
    public function allowsNull() {
        return $this->allowsNull;
    }

    /**
     * @return string
     */
    public function getDefaultValue() {
        return $this->defaultValue;
    }

    /**
     * @return boolean
     */
    public function hasDefaultValue() {
        return $this->allowsNull || $this->defaultValue !== null;
    }

    /**
     * @return boolean
     */
    public function isPassedByReference() {
        return $this->isPassedByReference;
    }
}
